/*
* This file is a part of the open-eBackup project.
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at
* http://mozilla.org/MPL/2.0/.
*
* Copyright (c) [2024] Huawei Technologies Co.,Ltd.
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
*/
#include "ApsaraVolumeHandler.h"
#include "common/Structs.h"
#include "volume_handlers/common/DiskDeviceFile.h"
#include "volume_handlers/common/DiskCommDef.h"


namespace {
    const std::string MODULE_NAME = "ApsaraVolumeHandler";
    const uint32_t GB = 1024 * 1024 * 1024UL;
    const uint32_t QUERY_CREATE_VOL_RETRY_INTERVAL = 240;
    const uint32_t PER_GB_RETYR_TIMES = 5;
    const uint32_t RETRY_COUNT = 3;
    const uint32_t SLEEP_TIMES = 10;
    const uint32_t DEFAULT_RETRY_INTEVAL = 60;
    constexpr int64_t INVOKE_API_FAILED_GENERAL_CODE = 1577213460;
    const std::string APSARASTACK_API_CLASS = "ResourceApiWrapper";
}

namespace ApsaraStackPlugin {

int32_t ApsaraVolumeHandler::DoAttachVolumeToHost(const std::string &diskId)
{
    INFOLOG("Begin to attach disk(%s) to server(%s).", diskId.c_str(), m_serverId.c_str());
    std::lock_guard<std::mutex> lock(m_attachVolumeMutex);
    PyObject* pArgs;
    {
        std::lock_guard<std::mutex> guard(ExecutePython::m_pythonMutex);
        PyAllowThreads thread;
        pArgs = Py_BuildValue("ss", m_serverId.c_str(), diskId.c_str());
    }
    int32_t ret = SUCCESS;
    TP_START("TP_AttachVolume", 1, &ret);
    TP_END
    if (ret == FAILED) {
        return FAILED;
    }
    std::string rspStr;
    if (ExecutePython::GetInstance()->CallObjFunction(APSARASTACK_API_CLASS, "attach_disk",
        pArgs, rspStr) != SUCCESS) {
        ERRLOG("Call python funtion attach_disk failed.");
        return FAILED;
    }
    std::vector<std::string> intermediateState = {"Attaching"};
    if (DoWaitVolumeStatus(diskId, "In_use", intermediateState) != SUCCESS) {
        ERRLOG("Wait volume(%s) status in-use timeout.", diskId.c_str());
        return FAILED;
    }
    INFOLOG("Attach disk(%s) to Host success.", diskId.c_str());
    return SUCCESS;
}

std::string ApsaraVolumeHandler::GetProxyHostId()
{
    return Utils::GetProxyHostId(false);
}

int32_t ApsaraVolumeHandler::DoCreateNewVolumeFromSnapShotId(const VolSnapInfo &snapshot)
{
    std::string diskName = GenNewVolumeName(snapshot.m_snapshotId);
    std::string description = GetNewVolumeDesciption();
    std::string diskType = m_volInfo.m_volumeType;
    int32_t diskSize = m_volSizeInBytes / GB;
    PyObject* pArgs;
    {
        std::lock_guard<std::mutex> guard(ExecutePython::m_pythonMutex);
        PyAllowThreads thread;
        pArgs = Py_BuildValue("ssssiss", m_volInfo.m_location.c_str(), snapshot.m_snapshotId.c_str(),
            m_volInfo.m_extendInfo.c_str(), diskName.c_str(), diskSize, diskType.c_str(), description.c_str());
    }
    std::string rspStr;
    if (ExecutePython::GetInstance()->CallObjFunction(APSARASTACK_API_CLASS, "create_disk",
        pArgs, rspStr) != SUCCESS) {
        ERRLOG("Call python funtion failed.");
        return FAILED;
    }
    APSCreateDiskResponse response;
    if (Utils::TransStrToApsaraApiResponse(rspStr, response) != SUCCESS) {
        ERRLOG("Failed to parse create disk function result.");
        return FAILED;
    }
    m_newCreateVolId = response.m_diskId;
    uint32_t interval = Module::ConfigReader::getInt("ApsaraStackConfig", "CreateDiskWaitInterval");
    uint32_t retryTimes = Module::ConfigReader::getInt("ApsaraStackConfig", "CreateDiskWaitRetryTimes");
    interval == 0 ? interval = QUERY_CREATE_VOL_RETRY_INTERVAL : interval;
    retryTimes == 0 ? retryTimes = (m_volSizeInBytes / GB) * PER_GB_RETYR_TIMES  : retryTimes;
    std::vector<std::string> interState = {"Creating"};
    int ret = DoWaitVolumeStatus(response.m_diskId, "Available", interState,
        interval, retryTimes);
    if (ret != SUCCESS) {
        ERRLOG("Disk Status is not available.");
        m_reportArgs = { snapshot.m_snapshotId, response.m_diskId};
        m_reportPara = {
            "virtual_plugin_cinder_volume_create_volume_failed_label",
            JobLogLevel::TASK_LOG_ERROR,
            SubJobStatus::FAILED, 0, 0, INVOKE_API_FAILED_GENERAL_CODE};
        return FAILED;
    }
    Disk disk;
    GetDiskDetail(response.m_diskId, disk);
    Utils::ConvertDiskToVolume(disk, m_newCreateVolInfo);
    UpdateNewDiskInfoToFile(disk);
    return SUCCESS;
}

int32_t ApsaraVolumeHandler::DoWaitVolumeStatus(const std::string &diskId, const std::string &status,
    std::vector<std::string> intermediateState, uint32_t interval, uint32_t retryCount)
{
    int32_t retryTime = 0;
    while (retryTime < retryCount) {
        Disk diskInfo;
        if (!GetDiskDetail(diskId, diskInfo)) {
            ERRLOG("Get disk %s detail failed.", diskId.c_str());
            return FAILED;
        }
        if (diskInfo.m_status == status) {
            INFOLOG("Disk(%s) status is %s.", diskId.c_str(), status.c_str());
            return SUCCESS;
        }
        ExpandWaitTime(retryTime, retryCount, diskInfo.m_status, intermediateState);
        INFOLOG("Disk(%s) status(%s) is not %s", diskId.c_str(),
            diskInfo.m_status.c_str(), status.c_str());
        retryTime++;
        sleep(interval);
    }
    return FAILED;
}

int32_t ApsaraVolumeHandler::DoDetachVolumeFromeHost(const std::string &detachVolId, const std::string &serverId)
{
    PyObject* pArgs;
    {
        std::lock_guard<std::mutex> guard(ExecutePython::m_pythonMutex);
        PyAllowThreads thread;
        pArgs = Py_BuildValue("ss", serverId.c_str(), detachVolId.c_str());
    }
    std::string rspStr;
    if (ExecutePython::GetInstance()->CallObjFunction(APSARASTACK_API_CLASS,
        "detach_disk", pArgs, rspStr) != SUCCESS) {
        ERRLOG("Call python funtion detach_disk failed.");
        return FAILED;
    }
    if (DoWaitVolumeStatus(detachVolId, APSARA_DISK_AVAILABLE_STATUS) != SUCCESS) {
        ERRLOG("Wait volume(%s) available timeout.", detachVolId.c_str());
        return FAILED;
    }
    return SUCCESS;
}

bool ApsaraVolumeHandler::GetDiskDetail(const std::string &diskId, Disk &diskDetail)
{
    PyObject* pArgs;
    {
        std::lock_guard<std::mutex> guard(ExecutePython::m_pythonMutex);
        PyAllowThreads thread;
        pArgs = Py_BuildValue("ssiis", m_volInfo.m_location.c_str(), "", 1, 10, diskId.c_str());
    }
    std::string rspStr;
    if (ExecutePython::GetInstance()->CallObjFunction(APSARASTACK_API_CLASS, "list_disks",
        pArgs, rspStr) != SUCCESS) {
        ERRLOG("Call python funtion failed.");
        return false;
    }
    DiskResponse response;
    if (Utils::TransStrToApsaraApiResponse(rspStr, response) != SUCCESS) {
        ERRLOG("Failed to parse create disk function result.");
        return false;
    }
    if (response.m_total == 0) {
        ERRLOG("Disk %s dont find.", diskId.c_str());
        return false;
    }
    diskDetail = response.m_disks.m_disk[0];
    INFOLOG("Get disk %s info success.", diskId.c_str());
    return true;
}

bool ApsaraVolumeHandler::WaitDiskStatus(const Disk &volDetail, std::string &status)
{
    status = APSARA_DISK_AVAILABLE_STATUS;
    if (volDetail.m_status == APSARA_DISK_DETACHING_STATUS) {
        if (DoWaitVolumeStatus(m_newCreateVolInfo.m_id, APSARA_DISK_AVAILABLE_STATUS) != SUCCESS) {
            ERRLOG("Wait detaching volume(%s) available timeout", m_newCreateVolInfo.m_id.c_str());
            return false;
        }
        return true;
    }
    if (volDetail.m_attachMents.m_attachment[0].m_instanceId != m_serverId) {
        if (DetachVolumeHandle(m_newCreateVolInfo.m_id, volDetail.m_attachMents.m_attachment[0].m_instanceId)
            != SUCCESS) {
            ERRLOG("Disk has been attach other host,  Detach disk(%s) from other host(%s) failed.",
                m_newCreateVolInfo.m_id.c_str(), volDetail.m_attachMents.m_attachment[0].m_instanceId.c_str());
            return false;
        }
    } else {
        if (DoWaitVolumeStatus(m_newCreateVolInfo.m_id, APSARA_DISK_IN_USE_STATUS) != SUCCESS) {
            ERRLOG("Wait volume(%s) status in-use timeout.", m_newCreateVolInfo.m_id.c_str());
            return false;
        }
        status = APSARA_DISK_IN_USE_STATUS;
    }
    return true;
}

bool ApsaraVolumeHandler::CheckAndAttachVolume()
{
    Disk diskInfo;
    if (!GetDiskDetail(m_newCreateVolId, diskInfo)) {
        ERRLOG("Get disk(%s) detail failed.", m_newCreateVolId.c_str());
        return false;
    }
    INFOLOG("Disk(%s) status is %s.", m_newCreateVolId.c_str(), diskInfo.m_status.c_str());
    if (!diskInfo.m_attachMents.m_attachment.empty()) {
        std::string status;
        if (!WaitDiskStatus(diskInfo, status)) {
            ERRLOG("Wait disk(%s) status ready failed.", m_newCreateVolId.c_str());
            return false;
        }
        if (status == m_volumeInUseStatus) {
            return true;
        }
    }
    uint32_t interval = Module::ConfigReader::getInt("ApsaraStackConfig", "CreateDiskWaitInterval");
    uint32_t retryTimes = Module::ConfigReader::getInt("ApsaraStackConfig", "CreateDiskWaitRetryTimes");
    interval == 0 ? interval = DEFAULT_RETRY_INTEVAL : interval;
    retryTimes == 0 ? retryTimes = (m_volSizeInBytes / GB) * PER_GB_RETYR_TIMES  : retryTimes;
    std::vector<std::string> interState = {};
    if (DoWaitVolumeStatus(m_newCreateVolId, m_voluemAvailableStatus, interState,
        interval, retryTimes) != SUCCESS) {
        ERRLOG("Wait disk(%s) available timeout.", m_newCreateVolId.c_str());
        return false;
    }
    for (int i = 0; i < RETRY_COUNT; i++) {
        if (DoAttachVolumeToHost(m_newCreateVolId) == SUCCESS) {
            INFOLOG("Attach Volume %s to host success, %s.", m_newCreateVolId.c_str(), m_taskId.c_str());
            return true;
        }
        WARNLOG("Attach Volume(%s) to host failed. serverId is (%s)", m_newCreateVolId.c_str(),
            m_serverId.c_str());
        sleep(SLEEP_TIMES);
    }
    m_reportArgs = { m_newCreateVolId, m_serverId};
    m_reportPara = {
                "virtual_plugin_cinder_volume_attach_volume_failed_label",
                JobLogLevel::TASK_LOG_ERROR,
                SubJobStatus::FAILED, 0, 0 };
    return false;
}

bool ApsaraVolumeHandler::CheckAndDetachVolume(const std::string &diskId)
{
    Disk diskInfo;
    if (!GetDiskDetail(diskId, diskInfo)) {
        ERRLOG("Get disk %s detail failed.", diskId.c_str());
        return false;
    }
    if (diskInfo.m_attachMents.m_attachment.empty()) {
        INFOLOG("Disk(%s) is available.", diskId.c_str());
        return true;
    }
    std::string curAttachInstance = diskInfo.m_attachMents.m_attachment[0].m_instanceId;
    if (DetachVolumeHandle(diskId, curAttachInstance) != SUCCESS) {
        ERRLOG("Detach disk(%s) from host(%s) failed.", diskId.c_str(), curAttachInstance.c_str());
        return false;
    }
    INFOLOG("Detach disk(%s) from host(%s) success.", diskId.c_str(), curAttachInstance.c_str());
    return true;
}

bool ApsaraVolumeHandler::UpdateNewDiskInfoToFile(Disk &newCreateDisk)
{
    std::lock_guard<std::mutex> lock(m_fileMutex);
    if (m_volumeInfoRepoHandler->Exists(m_snapshotCreateVolumeFile)) {
        if (Utils::LoadFileToStructWithRetry(m_volumeInfoRepoHandler, m_snapshotCreateVolumeFile,
            m_newCreatedVolumeList) != SUCCESS) {
            ERRLOG("Load create disk list info failed.");
            return false;
        }

        std::vector<Volume>::iterator it;
        for (it = m_newCreatedVolumeList.m_volumelist.begin();
            it != m_newCreatedVolumeList.m_volumelist.end(); it++) {
            if ((*it).m_snapshotId == newCreateDisk.m_sourceSnapshotId && (*it).m_id == newCreateDisk.m_id) {
                m_newCreatedVolumeList.m_volumelist.erase(it);
                break;
            }
        }
    }
    Volume volume;
    Utils::ConvertDiskToVolume(newCreateDisk, volume);
    m_newCreatedVolumeList.m_volumelist.push_back(volume);

    std::string newCreatedVolumeListStr;
    if (!Module::JsonHelper::StructToJsonString(m_newCreatedVolumeList, newCreatedVolumeListStr)) {
        ERRLOG("Convert created volume list to json string failed.");
        return false;
    }

    if (Utils::SaveToFileWithRetry(m_volumeInfoRepoHandler, m_snapshotCreateVolumeFile,
        newCreatedVolumeListStr) != SUCCESS) {
        ERRLOG("Save created volume list failed.");
        return false;
    }
    return true;
}
}